<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>日历</title>
  <style>
    * {
      margin: 0; padding: 0; box-sizing: border-box;
      font-family: Arial;
      touch-action: pan-y;
    }
    body {
      background: #F0F0FA;
      display: flex; justify-content: center; align-items: center;
      min-height: 100vh; padding: 10px;
      user-select: none;
    }
    .calendar {
      background: #fff; border-radius: 10px;
      width: 100%; max-width: 350px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, .1);
      overflow: hidden;
    }
    .calendar-header {
      display: flex; justify-content: space-between; align-items: center;
      padding: 8px; background: linear-gradient(90deg, #AABEFF 0%, #8296FF);
      color: #fff;
    }
    .calendar-header button {
      background: none; border: none; color: #fff;
      font-size: 20px; padding: 0 12px; border-radius: 8px;
      transition: transform 0.2s, background-color 0.2s;
    }
    .calendar-header button:active {
      transform: scale(0.9);
      background-color: rgba(255, 255, 255, 0.2);
    }
    .month-year {
      font-size: 18px; color: rgba(255, 255, 255, .9);
      text-align: center;
    }
    .days-of-week {
      display: grid; grid-template-columns: repeat(7, 1fr);
      text-align: center; padding: 6px;
      border-bottom: 1px solid #e9ecef; background: #FAFAFA;
      color: #6E6E82;
    }
    .calendar-slider {
      position: relative;
      overflow: hidden;
      width: 100%;
      transition: height 0.3s;
    }
    .calendar-slider-inner {
      display: flex;
      height: 100%;
    }
    .calendar-month-view {
      width: 100%;
      flex-shrink: 0;
      padding: 6px;
    }
    .days-grid {
      display: grid;
      grid-template-columns: repeat(7, 1fr);
      gap: 6px;
    }
    .day {
      position: relative;
      aspect-ratio: 1/1;
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: 10px;
      background-color: rgba(0, 0, 0, .02);
      color: #6E6E82;
      transition: all 0.3s ease;
    }
    .calendar-slider {
      height: calc((350px) / 7 * 6);
    }
    .calendar-slider.expanded {
      height: calc((350px) / 7 * 10 - 3px);
    }
    .day.active { 
      -webkit-text-stroke: 0.5px;
      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.15) inset;
    }
    .out-of-month { color: rgba(110, 110, 130, .5); }
    .three-day { color: #8296FF; }
    .out-of-month.three-day { color: rgba(130, 150, 255, .5); }
    .today { 
      box-shadow: 0 0 0 3px #8296FF inset;
    }
    .day.today.active { box-shadow: 0 0 0 2px #8296FF inset, 0 0 0 4px rgba(0, 0, 255, 0.15) inset; }
    .holiday { background-color: #DCFFDC !important; }
    .cyclic-weekend { background-color: #DCE1FF !important; }
    .holiday.cyclic-weekend { 
      background: linear-gradient(135deg, #DCE1FF 20%, #DCFFDC 100%) !important;
    }
    .action-bar {
      display: flex; justify-content: space-between; align-items: center;
      padding: 8px; background: linear-gradient(90deg, #8296FF 0%, #AABEFF);
      color: rgba(255, 255, 255, .9);
    }
    .rest-tip {
      max-width: 60%; white-space: nowrap; overflow: hidden;
      padding: 5px 8px; font-size: 15px; text-overflow: ellipsis;
    }
    .action-buttons { display: flex; }
    .expand-btn, .today-btn {
      background: none; border: none; color: rgba(255, 255, 255, .9);
      padding: 5px 8px; border-radius: 8px; font-size: 15px;
      transition: transform 0.2s, background-color 0.2s;
    }
    .expand-btn:active, .today-btn:active {
      transform: scale(0.9);
      background-color: rgba(255, 255, 255, 0.2);
    }
  </style>
</head>
<body>
  <div class="calendar" id="c">
    <div class="calendar-header">
      <button id="prev">&lt;</button>
      <div class="month-year" id="my"></div>
      <button id="next">&gt;</button>
    </div>
    <div class="days-of-week">
      <div>一</div><div>二</div><div>三</div><div>四</div><div>五</div><div>六</div><div>日</div>
    </div>
    <div class="calendar-slider" id="calendarSlider">
      <div class="calendar-slider-inner" id="slider"></div>
    </div>
    <div class="action-bar">
      <div class="rest-tip" id="restTip"></div>
      <div class="action-buttons">
        <button class="today-btn" id="todayBtn">今日</button>
        <button class="expand-btn" id="expand">展开</button>
      </div>
    </div>
  </div>

  <script>
    let d = new Date(), today = new Date(), three = new Set(), cyclic = new Set(), holidays = new Set(), holidayNames = {}, isExpanded = false, selectedDay = null, currentX = -100;
    const CYCLE = 21, RANGE = 3;
    
    const holidayData = [
      { year: 2025, month: 1, days: [1], name: "元旦" },
      { year: 2025, month: 1, days: [28,29,30,31], name: "春节" },
      { year: 2025, month: 2, days: [1,2,3,4], name: "春节" },
      { year: 2025, month: 4, days: [4,5,6], name: "清明节" },
      { year: 2025, month: 5, days: [1,2,3,4,5], name: "劳动节" },
      { year: 2025, month: 5, days: [31], name: "端午节" },
      { year: 2025, month: 6, days: [1,2], name: "端午节" },
      { year: 2025, month: 10, days: [1,2,3,4,5,6,7], name: "国庆节" },
      { year: 2025, month: 10, days: [8], name: "中秋节" },
    ];
    
    init();
    
    function init() {
      markAll();
      render(d);
      bindEvents();
      setupSwipe();
      selectToday();
    }
    
    
    function render(date) {
      slider.innerHTML = '';
      const y = date.getFullYear(), m = date.getMonth();
      
      my.textContent = isExpanded 
        ? `${y%100}年${m+1}月-${(m===11?y+1:y)%100}年${m===11?1:m+2}月`
        : `${y%100}年${m+1}月`;
      
      [-1, 0, 1].forEach(offset => {
        const viewDate = new Date(y, m + offset, 1);
        slider.appendChild(createMonthView(viewDate));
      });
      
      slider.style.transform = 'translateX(-100%)';
      
      if (selectedDay) {
        const activeDay = document.querySelector(`.day[data-date="${selectedDay}"]`);
        if (activeDay) activeDay.classList.add('active');
      }
    }
    
    function createMonthView(date) {
      const view = document.createElement('div');
      view.className = 'calendar-month-view';
      const grid = document.createElement('div');
      grid.className = 'days-grid';
      view.appendChild(grid);
      
      const y = date.getFullYear(), m = date.getMonth();
      const first = (new Date(y, m, 1).getDay() || 7);
      const daysInMonth = new Date(y, m+1, 0).getDate();
      
      fillDates(grid, y, m, first-1, daysInMonth);
      return view;
    }
    
    function fillDates(grid, y, m, prevDays, daysInMonth) {
      let cellCount = 0, currentDay = 1;
      const nextMonth = m === 11 ? 0 : m + 1;
      const nextYear = m === 11 ? y + 1 : y;
      const totalCells = isExpanded ? 70 : 42;
      
      for (let i = prevDays - 1; i >= 0 && cellCount < totalCells; i--) {
        const pm = m === 0 ? 11 : m - 1, py = m === 0 ? y - 1 : y;
        const day = new Date(py, pm + 1, 0).getDate() - i;
        addDayCell(grid, py, pm, day, 1);
        cellCount++;
      }
      
      while (currentDay <= daysInMonth && cellCount < totalCells) {
        addDayCell(grid, y, m, currentDay, 0);
        currentDay++;
        cellCount++;
      }
      
      let ny = nextYear, nm = nextMonth, nd = 1;
      while (cellCount < totalCells) {
        const days = new Date(ny, nm + 1, 0).getDate();
        if (nd > days) { nm = nm === 11 ? 0 : nm + 1; ny = nm === 0 ? ny + 1 : ny; nd = 1; }
        addDayCell(grid, ny, nm, nd, isExpanded && ny === nextYear && nm === nextMonth ? 0 : 1);
        nd++;
        cellCount++;
      }
    }
    
    function addDayCell(grid, y, m, d, isOutOfMonth) {
      const dateStr = `${y}-${m}-${d}`;
      const isToday = y === today.getFullYear() && m === today.getMonth() && d === today.getDate();
      let cls = 'day';
      if (isOutOfMonth) cls += ' out-of-month';
      if (three.has(dateStr)) cls += ' three-day';
      if (isToday) cls += ' today';
      if (selectedDay === dateStr) cls += ' active';
      
      if (holidays.has(dateStr) && cyclic.has(dateStr)) {
        cls += ' holiday cyclic-weekend';
      } else if (holidays.has(dateStr)) {
        cls += ' holiday';
      } else if (cyclic.has(dateStr)) {
        cls += ' cyclic-weekend';
      }
      
      const dayEl = document.createElement('div');
      dayEl.className = cls;
      dayEl.textContent = d;
      dayEl.dataset.date = dateStr;
      
      if (holidays.has(dateStr) && holidayNames[dateStr]) {
        dayEl.title = `${m+1}月${d}日 ${holidayNames[dateStr]}`;
      }
      
      dayEl.addEventListener('click', () => {
        document.querySelectorAll('.day.active').forEach(el => el.classList.remove('active'));
        selectedDay = dateStr;
        dayEl.classList.add('active');
        updateRestTip(dateStr);
      });
      
      grid.appendChild(dayEl);
    }
    

    function bindEvents() {
      prev.onclick = () => switchMonth('prev');
      next.onclick = () => switchMonth('next');
      
      expand.onclick = () => {
        if (isExpanded) {
          calendarSlider.classList.remove('expanded');
          calendarSlider.addEventListener('transitionend', function handleTransition() {
            calendarSlider.removeEventListener('transitionend', handleTransition);
            isExpanded = false;
            expand.textContent = '展开';
            render(d);
          }, { once: true });
        } else {
          isExpanded = true;
          expand.textContent = '收起';
     
          void calendarSlider.offsetHeight;
          
          calendarSlider.classList.add('expanded');
          render(d);
        }
      };

      todayBtn.onclick = () => {
        d = new Date();
        selectToday();
      };
    }

    function switchMonth(dir) {
      const isNext = dir === 'next';
      const targetX = isNext ? -200 : 0;
      const duration = 250;
      const startTime = performance.now();
      
      function animate(time) {
        const elapsed = time - startTime;
        const progress = Math.min(elapsed / duration, 1);
        const easeProgress = progress * (2 - progress);
        const newX = currentX + (targetX - currentX) * easeProgress;
        
        slider.style.transform = `translateX(${newX}%)`;
        
        if (progress < 1) {
          requestAnimationFrame(animate);
        } else {
          completeAnimation(isNext);
        }
      }
      
      requestAnimationFrame(animate);
    }

    function completeAnimation(isNext) {
      if (isNext) {
        d.setMonth(d.getMonth() + 1);
        slider.firstChild.remove();
        slider.appendChild(createMonthView(new Date(d.getFullYear(), d.getMonth() + 1, 1)));
      } else {
        d.setMonth(d.getMonth() - 1);
        slider.lastChild.remove();
        slider.insertBefore(createMonthView(new Date(d.getFullYear(), d.getMonth() - 1, 1)), slider.firstChild);
      }
      
      updateMonthDisplay();
      currentX = -100;
      slider.style.transform = 'translateX(-100%)';
      
      if (selectedDay) {
        const activeDay = document.querySelector(`.day[data-date="${selectedDay}"]`);
        if (activeDay) activeDay.classList.add('active');
      }
    }
    
    function updateMonthDisplay() {
      const y = d.getFullYear(), m = d.getMonth();
      my.textContent = isExpanded 
        ? `${y%100}年${m+1}月-${(m===11?y+1:y)%100}年${m===11?1:m+2}月`
        : `${y%100}年${m+1}月`;
    }
    
    function setupSwipe() {
    let startX = null;
  c.addEventListener('touchstart', e => {
    startX = e.touches[0].clientX;
    currentX = -100;
  });
  
  c.addEventListener('touchmove', e => {
    if (!startX) return;
    const diff = startX - e.touches[0].clientX;
    currentX = -100 - diff / 3;
    slider.style.transform = `translateX(${currentX}%)`;
    updateMonthDisplay();
  });
  
  c.addEventListener('touchend', e => {
    if (!startX) return;
    const endX = e.changedTouches[0].clientX;
    const diff = startX - endX;
    
    if (Math.abs(diff) > 0) {
      switchMonth(diff > 0 ? 'next' : 'prev');
    } else {
      currentX = -100;
      slider.style.transform = 'translateX(-100%)';
    }
    startX = null;
  });
}

    function markAll() {
      three.clear(); cyclic.clear(); holidays.clear(); holidayNames = {};
      
      const start = new Date(2025, 11, 2);
      const early = new Date(start.getFullYear()-RANGE, start.getMonth(), start.getDate());
      const late = new Date(start.getFullYear()+RANGE, start.getMonth(), start.getDate());

      
      for (let curr = new Date(early); curr <= late; curr.setDate(curr.getDate()+1)) {
        const y = curr.getFullYear(), m = curr.getMonth(), d = curr.getDate();
        const dow = curr.getDay()||7, doff = Math.floor((curr - early)/86400000);
        
        if (doff % 3 === 0) three.add(`${y}-${m}-${d}`);
        
        const cyclePos = doff % CYCLE;
        if ((cyclePos < 7 && dow === 7) || (cyclePos >=7 && cyclePos <14 && dow ===6)) {
          cyclic.add(`${y}-${m}-${d}`);
        }
      }
      
      holidayData.forEach(holiday => {
        holiday.days.forEach(day => {
          const dateStr = `${holiday.year}-${holiday.month-1}-${day}`;
          holidays.add(dateStr);
          holidayNames[dateStr] = holiday.name;
        });
      });
    }
    
    function selectToday() {
      selectedDay = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
      updateRestTip(selectedDay);
      render(d);
    }
    
    function updateRestTip(dateStr) {
      const [y, m, d] = dateStr.split('-').map(Number);
      let tipText = `${m+1}月${d}号`;
      
      if (three.has(dateStr) || cyclic.has(dateStr)) {
        tipText += '休息';
      } else {
        tipText += '上班';
      }
      
      if (holidays.has(dateStr) && holidayNames[dateStr]) {
        tipText = `${m+1}月${d}号${holidayNames[dateStr]}休息`;
      }
      
      restTip.textContent = tipText;
    }
  </script>
</body>
</html>